跳到主要内容

2.2-React 面试题

基础篇

render 函数中 return 如果没有使用 () 会有什么问题?

babel 会将 JSX 语法编译成 js,同时会在每行自动添加分号 ;,此时会返回 undefined

React-Router 怎么获取 URL 的参数

可以使用 location.search 获取浏览器的查询字符串,然后使用浏览器自带的 new URLSearchParam() 去解析即可得到 URL 参数

你简历里是 React 和 Vue 都会,那先说说你是怎么看这两个框架的把

什么叫渲染劫持

渲染劫持的概念是控制组件从另一个组件输出的能力,当然这个概念一般和react中的高阶组件(HOC)放在一起解释比较明了。

高阶组件可以在 render 函数中做非常多的操作,从而控制原组件的渲染输出,只要改变了原组件的渲染,我们都将它称之为一种渲染劫持。

实际上,在高阶组件中,组合渲染和条件渲染都是渲染劫持的一种,通过反向继承,不仅可以实现以上两点,还可以增强由原组件render函数产生的React元素。

使用 Hooks 要遵守哪些原则

  • hooks 必须以 use 开头
  • 只能在函数式组件,或者其它 hooks 中使用
  • 不能在循环内,条件,或者函数调用中使用

useEffect 和 useLayoutEffect 有什么区别?

大多数情况下两者是相同的,建议使用 useEffect,只有在改变布局的时候,放止布局闪烁,此时使用 useLayoutEffect,会在绘制屏幕之前触发

严格模式有什么用处?

在开启严格模式后,会对同一个组件进行两次渲染,确定组件是纯粹的,有助于识别过时的 API,

Fragment 有什么用

不想使用 div 等包裹一整个组件的时候,使用 <></> 会渲染到一个同一级

immutable

不可变数据,在字典树等大型数据结构中,如果拷贝一份需要很大的成本,只对更改的部分进行替换,降低性能成本。

  • 降低了Mutable带来的复杂度;
  • 节省内存;
  • Undo/RedoCopy/Paste,甚至时间旅行这些功能做起来都是小菜一碟;
  • 并发安全;
  • 拥抱函数式编程;

什么是事件合成机制

原因

  • 统一不同浏览器的事件处理窗口 event.target 的行为
  • 浏览器兼容问题

性能优化

  • 事件委托:将事件绑定到根节点,而非每个元素
    • 16.x 及之前,所有事件委托到 document
    • 17.x 之后,委托到应用根 DOM 节点,避免全局污染
  • 事件池:复用事件对象,减少内存开销

扩展能力

  • 支持自定义事件,如 onDoubleClick
  • 高级功能(事件优先调度)

事件触发机制:原生事件触发 → 根节点捕获事件 → React 生成 SyntheticEvent → 收集事件监听器 → 按组件树冒泡/捕获顺序执行

这样的执行顺序,也确保了 createPortal

事件对象

interface SyntheticEvent {
nativeEvent: Event; // 原生事件对象
currentTarget: DOMElement; // 事件绑定的 React 元素
target: DOMElement; // 触发事件的 DOM 元素
type: string; // 事件类型(如 'click')
isDefaultPrevented(): boolean;
isPropagationStopped(): boolean;
persist(): void; // 禁用事件池化
}

事件池化示例

function handleClick(event) {
// ❌ 错误:异步访问事件属性
// setTimeout(() => {
// console.log(event.target); // null
// }, 100);

// ✅ 正确:保留事件引用
event.persist();
setTimeout(() => {
console.log(event.target); // 正常输出
}, 100);
}
原生事件(捕获) → 原生事件(目标) → React 事件(捕获) → React 事件(目标) → React 事件(冒泡) → 原生事件(冒泡)

组件更新触发条件与渲染优化

触发更新 -> 生成虚拟 DOM -> Diff 算法比较 -> 确定 DOM 更新范围 -> 提交到真实 DOM
  • State 变化:组件内部 useState/useReducer/this.setState 更新状态
  • Props 变化:父组件重新渲染导致传入的 props 值变化
  • Context 更新:组件订阅的 Context 数据发生变更
  • 父组件重新渲染:即使子组件的 props 未变化,父组件渲染仍可能导致子组件重新渲染(默认行为
  • Hooks 依赖变化useEffect/useMemo/useCallback 的依赖数组元素变更

渲染优化

使用 hooks 缓存

  • 虚拟滚动:使用react-windowreact-virtualized
  • 拆分多个 Context,优化 Context 引起的渲染
const expensiveValue = useMemo(() => computeValue(a, b), [a, b]);
// 缓存函数引用
const handleClick = useCallback(() => {
// 依赖 a 但保持引用稳定
}, [a]);
const value = useContextSelector(MyContext, v => v.requiredField);

为什么父组件更新会导致所有子组件渲染?如何避免?

react 默认采用"render and diff"策略,使用React.memo阻断无效更新

  • 优先解决重复渲染问题:使用React DevTools Profiler定位关键路径
  • 避免过早优化:只在性能瓶颈出现时实施优化
  • 保持组件纯净:减少渲染过程中的副作用操作
  • 控制渲染范围:使用children props阻断无关更新

熟练掌握级

hooks 的设计目标

  • 逻辑复用:解决类组件中高阶组件(HOC)和Render Props的嵌套地狱问题

  • 简化组件:告别 this 绑定和生命周期方法的分散逻辑

  • 函数式优先:拥抱函数式编程范式,提升代码可预测性

  • 存储结构:Hooks数据存储在Fiber节点的memoizedState属性中,通过单向链表管理

  • 执行顺序依赖:Hooks调用顺序在每次渲染中必须严格一致(链表顺序不可变)

  • 闭包陷阱:每个Hooks闭包捕获当次渲染的props/state快照

react 的调度机制

scheduler:(协调器)是react的更新流程中非常重要的一环。只需要将任务和任务的优先级交给它,它就可以帮你管理任务,安排任务的执行。

  • 优先级调度:Hooks 更新请求会被 Scheduler 模块根据优先级(Immediate/UserBlocking/Normal)排队处理
  • 批量更新:React 自动合并多个 setState 调用,减少渲染次数

在涉及到例如大量的 dom 更新操作,如果一直同步执行这个耗时非常久的任务,就会一直占用着线程,所以就会造成用户在使用浏览器时视觉上的卡顿。

scheduler对于更新的方式上做出优化:对于单个任务来说,会有节制地去执行,不会一直占用着线程去执行任务。而是执行一会,中断一下,再执行,一直重复。而对于多个任务,它会先执行高优先级任务。

对于scheduler的执行特性,可以看出来主要是对两种形式进行优化:多个任务之间的管理和单个任务的执行控制。这也就引申出来scheduler两种概念:时间片、任务优先级

时间片:时间片是指在单个任务在这一帧内最大的执行时间,超过这个时间后会立即被打断,不会一直占用线程,这样页面就不会因为任务连续执行的时间过长而产生视觉上的卡顿。

优先级:指在有多个任务待执行时,按照优先级的顺序依次执行,这样可以使一些紧急任务先被执行。

既然存在着优先级的概念。那么必然存在一个任务队列对所有的任务进行管理,按照某种顺序对所有的任务进行排序。在scheduler中存在着两种队列,分别对不同的任务进行管理。

// 优先级划分
// 无优先级
export const NoPriority = 0;
// 最高优先级 立即执行
export const ImmediatePriority = 1;
// 用户阻塞级别的优先级
export const UserBlockingPriority = 2;
// 常规优先级
export const NormalPriority = 3;
// 较低优先级
export const LowPriority = 4;
// 空闲优先级 可闲置的任务
export const IdlePriority = 5;

scheduler 接受任务后,会创建一个任务对象,保存优先级,开始时间,过期时间等内容。

用户交互(如输入)优先于数据更新(如 API 响应)

当所有要执行的任务被确定后,会被分为两种,

  • 未过期:timerQueue,过期时间在当前时间之后,开始时间越小越靠前。开始时间默认是当前时间,如果进入调度的时候传了延迟时间,当前时间与延迟时间的和作为开始时间
  • 已过期:taskQueue,当前时间超过过期时间,过期时间越小越靠前。

taskQueue 队列中的函数会挨个立即执行

timberQueue 队列中的任务会等待第一个任务的时间到了,加入到 taskQueue 中,然后重复执行,直到 timber 队列被清空

scheduleCallback 是整个调度的入口函数,主要负责生成调度任务、根据任务是否过期将任务放入

单个任务的执行,受执行者控制,一旦某个任务的执行时间超出时间片的限制。就会被中断,然后当前的执行者退出,退出之前会通知调度者再去调度一个新的执行者继续完成这个任务,新的执行者在执行任务时依旧会根据时间片中断任务,然后退出,重复这一过程,直到当前这个任务彻底完成后,将任务从taskQueue出队。

fiber 是什么

旧版协调算法导致的性能瓶颈

  • 递归不可中断:同步遍历整个虚拟 DOM 树,长时间占用主线程
  • 卡顿问题:复杂组件树更新导致掉帧(如大型列表、动画场景)

新增一些内容

  • 优先级调度:用户交互(如输入)优先于数据更新(如API响应)
  • 可恢复工作单元:保存中间状态,允许暂停/恢复渲染流程

fiber 节点的主要数据结构

  • type:组件类型(如 div、函数组件引用)
  • stateNode:对应的 DOM 节点或类组件实例
  • child:第一个子节点(Fiber 节点)
  • sibling:下一个兄弟节点(Fiber 节点)
  • return:父节点(Fiber 节点)
  • pendingProps:新传入的 props
  • memoizedProps:上一次渲染使用的 props
  • memoizedState:上一次渲染后的 state(如 Hooks 链表)
  • effectTag:标记副作用类型(如 PlacementUpdateDeletion
  • alternate:指向当前 Fiber 的镜像(用于 Diff 比较)(Fiber 节点)

React 维护两颗 Fiber 树已确保更新无冲突

  • Current Tree:当前已渲染的 UI 对应的 Fiber 树
  • WorkInProgress Tree:正在构建的新Fiber树

面试实况

对 React 和 Vue 的看法

在开发模式上,React 提出了 JSX 这种开发模式,并且用了很多年时间让社区去接受,而 Vue 则是采用现成的模版语法的开发模式。但感觉就这两年这两个框架都在往一个函数式组件的方向实现,Vue3 中的函数式组件甚至在某种层面上说比 react 更自由。

在实现层面上说的话,就那一套,单向数据流,双向绑定,数据不可变性,更智能的依赖收集,优化的方式不同

听到你说了更自由和智能的依赖收集,具体指的是?

"比如 react useEffect 要手动加依赖,但 vue3 的 watchEffect 和 computed 就不用"。

“自由方面,hook 得顶层,不要条件循坏用,而且 react 重心智负担:就闭包陷阱那一套过时变量嘛,依赖的选择,还有重复渲染这些问题,我一直不理解为什么不把这些心智负担收到框架里,我觉得 react 是有这个能力的。vue3 的话,你想咋用咋样 api 咋样,setup 也会只用一次,也不会像新旧树对比 hook 多次调用”

“哈哈感觉你对 react 怨气好大,因为我看你文章写了很多 react 源码的嘛,如果是你你会怎么去收敛这个心智负担到框架内部?”

就是 hook 顶层那个和条件循坏应该不好动,因为 react 不能去破坏 hooks 链表的结构,对于过时变量react18 已经将 useState 全改为异步了,依赖的选择的心智问题我觉得是否说可以更明确一点再未来加入配置项的话,将依赖的收集在 updateEffect 和 mountedEffect 前去提前收集,就做一个依赖系统专门去处理这个事情,感觉可以从编译器自动生成依赖数组,现在 react 只是一层浅比较。但其实这么想,大部分问题的根源,是React函数组件机制所限:每次组件渲染,组件里的所有代码都会被重新调用一次,在这上面其实也可以动下手(自己说了感觉当没说,感觉好尴尬啊硬吹)。就长话短说就是,react的心智模型让我要花很多精力去处理边界情况。

“其实你最后句话说得挺好的,因为 react 要求你用声明式的方式去写组件,你就不该去纠结引用会不会变,多添加个依赖很不舒服,重新渲染这种事情,你需要保证的是无论重新渲染多少次这个组件都不会变。假设你 useEffect 依赖于 AB,但你的 B 就可能只在首次创建后永远不变,它确实显得很“多余”但你不用纠结这个。真正的问题可能就在于你觉得的永远不会变只是你觉得,我们平时出现问题很多都是这种以为的边界问题导致 B 变造成的”

为什么 react 需要合成事件

“兼容性的考虑把,可以抹平不同浏览器事件对象差异,还有个就是避免了垃圾回收。”

“我们公司主要是 Vue,你的简历里Vue也更擅长一些,我们谈一下Vue把”

生命周期

随便了一下,每个生命周期,父子生命周期,每个生命周期的定义和写法。

参考文章

作者链接
Big shark@LXhttps://juejin.cn/post/7004638318843412493
haizlinhttps://github.com/haizlin/fe-interview
溪饱鱼年后被吊打的第一面